package com.common.servlet.abstracts;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.common.model.abstracts.AbstractRequest;
import com.common.security.abstracts.AbstractError;
import com.common.security.exceptions.HTTPException;
import com.common.security.exceptions.ParserException;
import com.common.servlet.interfaces.IParser;
import com.common.servlet.util.HTTPProtocolBase;

/**
 * The XMLParser converts all the XML files provided in their appropriate Request class. 
 * It can throw many exceptions and it's the servlet's job to interpretate and respond
 * with the apropriate message. The messages for the exceptions that the parser can
 * throw can be found in the ParserError.
 * 
 * @author Ady
 *
 */
public abstract class AbstractXMLParser implements IParser {
//TODO Trivial priority: add comments
	
	protected Document createDocument(InputStream stream) throws ParserException {
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			Document document = builder.parse(stream);
			document.getDocumentElement().normalize();
			
			return document;
		} catch (Exception e) {
			throw new ParserException(ParserException.STREAM_ERROR);
		}
	}
	
	protected Node getActionNode(Document document) throws ParserException {
		NodeList actions = document.getElementsByTagName(HTTPProtocolBase.ACTION);
		
		if (actions.getLength() > 1) {
			throw new ParserException(ParserException.MULTIPLE_ACTIONS_PROVIDED);
		} else if (actions.getLength() < 1) {
			throw new ParserException(ParserException.NO_ACTION_PROVIDED);
		}
		
		return actions.item(0);
	}
	
	protected Node getContentNode(Document document) throws ParserException {
		NodeList contents = document.getElementsByTagName(HTTPProtocolBase.CONTENT);
		
		if (contents.getLength() > 1) {
			throw new ParserException(ParserException.MULTIPLE_CONTENTS_PROVIDED);
		} else if (contents.getLength() < 1) {
			throw new ParserException(ParserException.NO_CONTENT_PROVIDED);
		}
		
		return contents.item(0);
	}
	
	abstract protected void insertAction(Node actionNode, AbstractRequest request) throws ParserException, HTTPException;
	
	protected Element getContent(Node contentNode) throws ParserException {
		if (contentNode.getNodeType() == Element.ELEMENT_NODE) {
			return (Element) contentNode;
		}
		throw new ParserException(ParserException.CONTENT_BAD_FORMAT);
	}
	
	protected Element getElement(Element element, String tagName) throws ParserException {
		NodeList list = element.getElementsByTagName(tagName);

		if (list.getLength() != 1) {
			throw new ParserException(ParserException.CONTENT_BAD_FORMAT);
		}
		
		Node node = list.item(0);
		
		if (node.getNodeType() == Node.ELEMENT_NODE) {
			return (Element) node;
		}
		throw new ParserException(ParserException.CONTENT_BAD_FORMAT);
	}
	
	/**
	 * Returns an element from the XML that has the name the same as the tagName. The
	 * parameter optional checks if this tag can be missing. If set to true, if the 
	 * element is not found, null will be returned. If it's set to true and the element
	 * is missing, a ParserException will be thrown.
	 * 
	 * @param element
	 * @param tagName
	 * @param optional
	 * @return
	 * @throws ParserException
	 */
	protected Element getElement(Element element, String tagName, boolean optional) throws ParserException {
		NodeList list = element.getElementsByTagName(tagName);
		
		if (list.getLength() > 1) {
			throw new ParserException(ParserException.CONTENT_BAD_FORMAT);
		}
		
		if (optional) {
			if (list.getLength() == 0) {
				return null;
			}
		} else {
			if (list.getLength() == 0) {
				throw new ParserException(ParserException.CONTENT_BAD_FORMAT);
			}
		}
		
		Node node = list.item(0);
		
		if (node.getNodeType() == Node.ELEMENT_NODE) {
			return (Element) node;
		}
		throw new ParserException(ParserException.CONTENT_BAD_FORMAT);
	}
	
	protected List<Element> getAllElements(Element element, String tagName) throws ParserException {
		NodeList list = element.getElementsByTagName(tagName);
		List<Element> elements = new ArrayList<Element>();
		Node node = null;
		
		for (int i = 0; i < list.getLength(); i++) {
			node = list.item(i);
			
			if (node.getNodeType() == Node.ELEMENT_NODE) {
				elements.add((Element) node);
			} else {
				throw new ParserException(ParserException.CONTENT_BAD_FORMAT);
			}
		}
		
		return elements;
	}
	
	public String parseError(AbstractError error) {
		StringBuilder builder = new StringBuilder();
		int code = error.getCode();
		String message = error.getMessage();
		String cause = error.getCause();
		
		builder.append(HTTPProtocolBase.RESPONSE_TAG);
		builder.append(HTTPProtocolBase.CLOSE_TAG);
		builder.append(HTTPProtocolBase.END_LINE);
		builder.append(getErrorCode(code, message, cause));
		builder.append(HTTPProtocolBase.END_LINE);
		builder.append(HTTPProtocolBase.RESPONSE_END_TAG);
		
		return builder.toString();
	}
	
	/**
	 * Get the error code tag.
	 * <ErrorCode value="" message="" cause="" />
	 * 
	 * @param code
	 * @return
	 */
	private String getErrorCode(int code, String message, String cause) {
		StringBuilder builder = new StringBuilder();
		
		builder.append(HTTPProtocolBase.ERROR_CODE_TAG);
		builder.append(HTTPProtocolBase.SPACE);
		builder.append(HTTPProtocolBase.VALUE_ATTRIBUTE);
		builder.append(code);
		builder.append(HTTPProtocolBase.END_ATTRIBUTE);
		builder.append(HTTPProtocolBase.SPACE);
		builder.append(HTTPProtocolBase.ERROR_MESSAGE_ATTRIBUTE);
		builder.append(message);
		builder.append(HTTPProtocolBase.END_ATTRIBUTE);
		builder.append(HTTPProtocolBase.SPACE);
		builder.append(HTTPProtocolBase.ERROR_CAUSE_ATTRIBUTE);
		builder.append(cause);
		builder.append(HTTPProtocolBase.END_ATTRIBUTE);
		builder.append(HTTPProtocolBase.SPACE);
		builder.append(HTTPProtocolBase.END_TAG);
		
		return builder.toString();
	}

	/**
	 * Return the item value as it should be displayed in an XML. This method should be
	 * overriden.
	 * 
	 * @param item
	 * @return
	 */
	protected <T> String eval(T item) {
		return item.toString();
	}
	
	protected String escapeXML(String value) {
		StringBuilder result = new StringBuilder();
		//TODO Trivial priority: replace with regex
		
		for (int i = 0; i < value.length(); i++) {
			switch (value.charAt(i)) {
				case '&': {
					result.append("&amp;");
					break;
				}
				case '\"': {
					result.append("&quot;");
					break;
				}
				case '\'': {
					result.append("&apos;");
					break;
				}
				case '<': {
					result.append("&lt;");
					break;
				}
				case '>': {
					result.append("&gt;");
					break;
				}
				default: {
					result.append(value.charAt(i));
					break;
				}
			}
		}
		
		return result.toString();
	}
}
